package com.jediterm.terminal.emulator;

import com.jediterm.terminal.*;
import com.jediterm.terminal.emulator.mouse.MouseFormat;
import com.jediterm.terminal.emulator.mouse.MouseMode;
import com.jediterm.terminal.util.CharUtils;
import com.jediterm.typeahead.Ascii;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.BiConsumer;

/**
 * The main com.jediterm.terminal emulator class.
 * <p/>
 * Obtains data from the  {@link TerminalDataStream}, interprets com.jediterm.terminal ANSI escape sequences as commands and directs them
 * as well as plain data characters to the  {@link Terminal}
 *
 * @author traff
 */

public class JediEmulator extends DataStreamIteratingEmulator {
    private static final Logger LOG = LoggerFactory.getLogger(com.jediterm.terminal.emulator.JediEmulator.class);

    private static int logThrottlerCounter = 0;
    private static int logThrottlerRatio = 100;
    private static int logThrottlerLimit = logThrottlerRatio;
    private final BlockingQueue<CompletableFuture<Void>> myResizeFutureQueue = new LinkedBlockingQueue<>();

    public JediEmulator(TerminalDataStream dataStream, Terminal terminal) {
        super(dataStream, terminal);
    }

    @Override
    public void processChar(char ch, Terminal terminal) throws IOException {
        switch (ch) {
            case 0:
                break;
            case Ascii.BEL: //Bell (Ctrl-G)
                terminal.beep();
                break;
            case Ascii.BS: //Backspace (Ctrl-H)
                terminal.backspace();
                break;
            case Ascii.CR: //Carriage return (Ctrl-M)
                terminal.carriageReturn();
                break;
            case Ascii.ENQ: //Return com.jediterm.terminal status (Ctrl-E). Default response is an empty string
                unsupported("Terminal status:" + escapeSequenceToString(ch));
                break;
            case Ascii.FF: //Form Feed or New Page (NP). Ctrl-L treated the same as LF
            case Ascii.LF: //Line Feed or New Line (NL). (LF is Ctrl-J)
            case Ascii.VT: //Vertical Tab (Ctrl-K). This is treated the same as LF.
                // '\n'
                terminal.newLine();
                break;
            case Ascii.SI: //Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0 character set (the default)
                //LS0 (locking shift 0)
                //Map G0 into GL
                terminal.mapCharsetToGL(0);
                break;
            case Ascii.SO: //Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the G1 character set (the default)
                //LS1 (locking shift 1)
                //Map G1 into GL
                if (Boolean.getBoolean("jediterm.enable.shift_out.character.support")) {
                    terminal.mapCharsetToGL(1);
                }
                break;
            case Ascii.HT: // Horizontal Tab (HT) (Ctrl-I)
                terminal.horizontalTab();
                break;
            case Ascii.ESC: // ESC
                processEscapeSequence(myDataStream.getChar(), myTerminal);
                break;
            default:
                if (ch <= Ascii.US) {
                    StringBuilder sb = new StringBuilder("Unhandled control character:");
                    CharUtils.appendChar(sb, CharUtils.CharacterType.NONE, ch);
                    unhandledLogThrottler(sb.toString());
                } else { // Plain characters
                    myDataStream.pushChar(ch);
                    String nonControlCharacters = myDataStream.readNonControlCharacters(terminal.distanceToLineEnd());

                    terminal.writeCharacters(nonControlCharacters);
                }
                break;
        }
        if (myDataStream.isEmpty()) {
            completeResize();
        }
    }

    private void processEscapeSequence(char ch, Terminal terminal) throws IOException {
        switch (ch) {
            case '[': // Control Sequence Introducer (CSI)
                ControlSequence args = new ControlSequence(myDataStream);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Control Sequence (" + args.getDebugInfo() + ")");
                }
                if (!args.pushBackReordered(myDataStream)) {
                    boolean result = processControlSequence(args);
                    if (!result) {
                        LOG.error("Unhandled Control Sequence (" + args.getDebugInfo() + ")");
                    }
                }
                break;
            case 'D': //Index (IND)
                terminal.index();
                break;
            case 'E': //Next Line (NEL)
                terminal.nextLine();
                break;
            case 'H': //Horizontal Tab Set (HTS)
                terminal.setTabStopAtCursor();
                break;
            case 'M': // Reverse Index (RI)
                terminal.reverseIndex();
                break;
            case 'N':
                terminal.singleShiftSelect(2); //Single Shift Select of G2 Character Set (SS2). This affects next character only.
                break;
            case 'O':
                terminal.singleShiftSelect(3); //Single Shift Select of G3 Character Set (SS3). This affects next character only.
                break;
            case 'P': // Device Control String (DCS)
                com.jediterm.terminal.emulator.SystemCommandSequence command = new com.jediterm.terminal.emulator.SystemCommandSequence(myDataStream);

                if (!deviceControlString(command)) {
                    LOG.error("Error processing DCS: ESCP" + command);
                }
                break;
            case ']': // Operating System Command (OSC)
                // xterm uses it to set parameters like windows title
                command = new com.jediterm.terminal.emulator.SystemCommandSequence(myDataStream);

                if (!operatingSystemCommand(command)) {
                    LOG.error("Error processing OSC: ESC]" + command);
                }
                break;
            case '6':
                unsupported("Back Index (DECBI), VT420 and up");
                break;
            case '7': //Save Cursor (DECSC)
                terminal.saveCursor();
                break;
            case '8':
                terminal.restoreCursor();
                break;
            case '9':
                unsupported("Forward Index (DECFI), VT420 and up");
                break;
            case '=': //Application Keypad (DECKPAM)
                setModeEnabled(TerminalMode.Keypad, true);
                break;
            case '>': //Normal Keypad (DECKPNM)
                setModeEnabled(TerminalMode.Keypad, false);
                break;
            case 'F': //Cursor to lower left corner of the screen
                terminal.cursorPosition(1, terminal.getTerminalHeight());
                break;
            case 'c': //Full Reset (RIS)
                terminal.reset();
                break;
            case 'n': //Invoke the G2 Character Set as GL - locking shift 2 (LS2)
                myTerminal.mapCharsetToGL(2);
                break;
            case 'o': //Invoke the G3 Character Set as GL - locking shift 3 (LS3)
                myTerminal.mapCharsetToGL(3);
                break;
            case '|': //Invoke the G3 Character Set as GR - locking shift 3, right (LS3R)
                myTerminal.mapCharsetToGR(3);
                break;
            case '}': //Invoke the G2 Character Set as GR - locking shift 2, right (LS2R)
                myTerminal.mapCharsetToGR(2);
                break;
            case '~': //Invoke the G1 Character Set as GR - locking shift 1, right (LS1R)
                myTerminal.mapCharsetToGR(1);
                break;
            case '#':
            case '(':
            case ')':
            case '*':
            case '+':
            case '$':
            case '@':
            case '%':
            case '.':
            case '/':
            case ' ':
                processTwoCharSequence(ch, terminal);
                break;
            default:
                unsupported(ch);
        }
    }

    private boolean deviceControlString(com.jediterm.terminal.emulator.SystemCommandSequence args) {
        return false;
    }

    private boolean operatingSystemCommand(com.jediterm.terminal.emulator.SystemCommandSequence args) {
        int ps = args.getIntAt(0, -1);

        switch (ps) {
            case 0: // Icon name / Window Title
            case 1: // Icon name
            case 2: // Window Title
                // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
                String name = args.getStringAt(1);
                if (name != null) {
                    myTerminal.setWindowTitle(name);
                    return true;
                }
                break;
            case 7:
                // Support for OSC 7 is pending
                // "return true" to avoid logging errors about unhandled sequences;
                return true;
            case 8: // Hyperlink https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
                String uri = args.getStringAt(2);
                if (uri != null) {
                    if (!uri.isEmpty()) {
                        myTerminal.setLinkUriStarted(uri);
                    } else {
                        myTerminal.setLinkUriFinished();
                    }
                    return true;
                }
                break;
            case 10:
            case 11:
                return processColorQuery(args);
        }
        return false;
    }


    /**
     * <a href="http://www.xfree86.org/4.8.0/ctlseqs.html">
     * If a "?" is given rather than a name or RGB specification, xterm replies with a control sequence of
     * the same form which can be used to set the corresponding dynamic color.
     * </a>
     */
    private boolean processColorQuery(@NotNull com.jediterm.terminal.emulator.SystemCommandSequence args) {
        if (!"?".equals(args.getStringAt(1))) {
            return false;
        }
        int ps = args.getIntAt(0, -1);
        TerminalColor color;
        if (ps == 10) {
            color = myTerminal.getWindowForeground();
        } else if (ps == 11) {
            color = myTerminal.getWindowBackground();
        } else {
            return false;
        }
        if (color != null) {
            String str = args.format(ps + ";" + formatXParseColor(color));
            if (LOG.isDebugEnabled()) {
                LOG.debug("Responding to OSC " + ps + " query: " + str);
            }
            myTerminal.deviceStatusReport(str);
        }
        return true;
    }

    /**
     * Returns <a href="https://linux.die.net/man/3/xparsecolor">XParseColor</a> representation of TerminalColor
     */
    private static @NotNull String formatXParseColor(TerminalColor color) {
        return "rgb:" + color.toHexString16();
    }

    private void processTwoCharSequence(char ch, Terminal terminal) throws IOException {
        char secondCh = myDataStream.getChar();
        switch (ch) {
            case ' ':
                switch (secondCh) {
                    //About different character sets: http://en.wikipedia.org/wiki/ISO/IEC_2022
                    case 'F': //7-bit controls
                        unsupported("Switching ot 7-bit");
                        break;
                    case 'G': //8-bit controls
                        unsupported("Switching ot 8-bit");
                        break;
                    //About ANSI conformance levels: http://www.vt100.net/docs/vt510-rm/ANSI
                    case 'L': //Set ANSI conformance level 1
                        terminal.setAnsiConformanceLevel(1);
                        break;
                    case 'M': //Set ANSI conformance level 2
                        terminal.setAnsiConformanceLevel(2);
                        break;
                    case 'N': //Set ANSI conformance level 3
                        terminal.setAnsiConformanceLevel(3);
                        break;

                    default:
                        unsupported(ch, secondCh);
                }
                break;
            case '#':
                switch (secondCh) {
                    case '8':
                        terminal.fillScreen('E');
                        break;
                    default:
                        unsupported(ch, secondCh);
                }
                break;
            case '%':
                switch (secondCh) {
                    case '@': // Select default character set. That is ISO 8859-1
                    case 'G': // Select UTF-8 character set
                        unsupported("Selecting charset is unsupported: " + escapeSequenceToString(ch, secondCh));
                        break;
                    default:
                        unsupported(ch, secondCh);
                }
                break;
            case '(':
                terminal.designateCharacterSet(0, secondCh); //Designate G0 Character set (VT100)
                break;
            case ')':
                terminal.designateCharacterSet(1, secondCh); //Designate G1 Character set (VT100)
                break;
            case '*':
                terminal.designateCharacterSet(2, secondCh); //Designate G2 Character set (VT220)
                break;
            case '+':
                terminal.designateCharacterSet(3, secondCh); //Designate G3 Character set (VT220)
                break;
            case '-':
                terminal.designateCharacterSet(1, secondCh); //Designate G1 Character set (VT300)
                break;
            case '.':
                terminal.designateCharacterSet(2, secondCh); //Designate G2 Character set (VT300)
                break;
            case '/':
                terminal.designateCharacterSet(3, secondCh); //Designate G3 Character set (VT300)
                break;
            case '$':
            case '@':
                unsupported(ch, secondCh);
        }
    }

    /**
     * This method is used to handle unknown sequences. Can be overridden.
     *
     * @param sequenceChars are the characters of the unhandled sequence following the ESC character
     *                      (first ESC is excluded from the sequenceChars)
     */
    protected void unsupported(char... sequenceChars) {
        unsupported(escapeSequenceToString(sequenceChars));
    }

    /**
     * This method is used to report about know unsupported sequences
     *
     * @param msg The message describing the sequence
     */
    private static void unsupported(String msg) {
        unhandledLogThrottler("Unsupported control characters: " + msg);
    }

    private static void unhandledLogThrottler(String msg) {
        logThrottlerCounter++;
        if (logThrottlerCounter < logThrottlerLimit) {
            if (logThrottlerCounter % (logThrottlerLimit / logThrottlerRatio) == 0) {
                if (logThrottlerLimit / logThrottlerRatio > 1) {
                    msg += " and " + (logThrottlerLimit / logThrottlerRatio) + " more...";
                }
                LOG.error(msg);
            }
        } else {
            logThrottlerLimit *= 10;
        }
    }

    private static String escapeSequenceToString(final char... b) {
        StringBuilder sb = new StringBuilder("ESC ");

        for (char c : b) {
            sb.append(' ');
            sb.append(c);
        }
        return sb.toString();
    }

    private boolean processControlSequence(ControlSequence args) {
        switch (args.getFinalChar()) {
            case '@':
                return insertBlankCharacters(args); //ICH
            case 'A':
                return cursorUp(args); //CUU
            case 'B':
                return cursorDown(args); //CUD
            case 'C':
                return cursorForward(args); //CUF
            case 'D':
                return cursorBackward(args); //CUB
            case 'E':
                return cursorNextLine(args); //CNL
            case 'F':
                return cursorPrecedingLine(args); //CPL
            case 'G':
            case '`':
                return cursorHorizontalAbsolute(args); //CHA
            case 'f':
            case 'H': //CUP
                return cursorPosition(args);
            case 'J': //DECSED
                return eraseInDisplay(args);
            case 'K': //EL
                return eraseInLine(args);
            case 'L': //IL
                return insertLines(args);
            case 'M': //DL
                return deleteLines(args);
            case 'X': //ECH
                return eraseCharacters(args);
            case 'P': //DCH
                return deleteCharacters(args);
            case 'S': //SU
                return scrollUp(args);
            case 'T': //SD
                return scrollDown(args);
            case 'c': //Send Device Attributes (Primary DA)
                if (args.startsWithMoreMark()) { //Send Device Attributes (Secondary DA)
                    if (args.getArg(0, 0) == 0) { //apply on to VT220 but xterm extends this to VT100
                        sendDeviceAttributes();
                        return true;
                    }
                    return false;
                }
                return sendDeviceAttributes();
            case 'd': //VPA
                return linePositionAbsolute(args);
            case 'g': // Tab Clear (TBC)
                return tabClear(args.getArg(0, 0));
            case 'h': //Set Mode (SM) or DEC Private Mode Set (DECSET)
                return setModeOrPrivateMode(args, true);
            case 'l': //Reset Mode (RM) or DEC Private Mode Reset (DECRST)
                return setModeOrPrivateMode(args, false);
            case 'm':
                if (args.startsWithMoreMark()) { //Set or reset resource-values used by xterm
                    // to decide whether to construct escape sequences holding information about
                    // the modifiers pressed with a given key
                    return false;
                }
                return characterAttributes(args); //Character Attributes (SGR)
            case 'n':
                return deviceStatusReport(args); //DSR
            case 'q':
                return cursorShape(args); //DECSCUSR
            case 'r':
                if (args.startsWithQuestionMark()) {
                    return restoreDecPrivateModeValues(args); //
                } else {
                    //Set Top and Bottom Margins
                    return setScrollingRegion(args); //DECSTBM
                }
            case 't':
                return windowManipulation(args);
            default:
                return false;
        }
    }

    private boolean windowManipulation(ControlSequence args) {
        // CSI Ps ; Ps ; Ps t
        switch (args.getArg(0, -1)) {
            case 8:
//        Ps = 8  ;  height ;  width -> Resize the text area to given
//        height and width in characters.  Omitted parameters reuse the
//        current height or width.  Zero parameters use the display's
//        height or width.
                int width = args.getArg(2, 0);
                int height = args.getArg(1, 0);
                if (width == 0) {
                    width = myTerminal.getTerminalWidth();
                }
                if (height == 0) {
                    height = myTerminal.getTerminalHeight();
                }
                myTerminal.resize(new Dimension(width, height), RequestOrigin.Remote);
                return true;
            case 22:
                return csi22(args);
            case 23:
                return csi23(args);
            default:
                return false;
        }
    }

    private boolean csi22(ControlSequence args) { // TODO: support icon title
        switch (args.getArg(1, -1)) {
            case 0: // Save xterm icon and window title on stack.
            case 2: // Save xterm window title on stack.
                myTerminal.saveWindowTitleOnStack();
                return true;
            case 1: // Save xterm icon title on stack.
                return true;
            default:
                return false;
        }
    }

    private boolean csi23(ControlSequence args) { // TODO: support icon title
        switch (args.getArg(1, -1)) {
            case 0: // Restore xterm icon and window title on stack.
            case 2: // Restore xterm window title on stack.
                myTerminal.restoreWindowTitleFromStack();
                return true;
            case 1: // Restore xterm icon title on stack.
                return true;
            default:
                return false;
        }
    }

    private boolean tabClear(int mode) {
        if (mode == 0) { //Clear Current Column (default)
            myTerminal.clearTabStopAtCursor();
            return true;
        } else if (mode == 3) {
            myTerminal.clearAllTabStops();
            return true;
        } else {
            return false;
        }
    }

    private boolean eraseCharacters(ControlSequence args) {
        myTerminal.eraseCharacters(args.getArg(0, 1));
        return true;
    }

    private boolean setModeOrPrivateMode(ControlSequence args, boolean enabled) {
        if (args.startsWithQuestionMark()) { // DEC Private Mode
            switch (args.getArg(0, -1)) {
                case 1: //Cursor Keys Mode (DECCKM)
                    setModeEnabled(TerminalMode.CursorKey, enabled);
                    return true;
                case 3: //132 Column Mode (DECCOLM)
                    setModeEnabled(TerminalMode.WideColumn, enabled);
                    return true;
                case 4: //Smooth (Slow) Scroll (DECSCLM)
                    setModeEnabled(TerminalMode.SmoothScroll, enabled);
                    return true;
                case 5: //Reverse Video (DECSCNM)
                    setModeEnabled(TerminalMode.ReverseVideo, enabled);
                    return true;
                case 6: //Origin Mode (DECOM)
                    setModeEnabled(TerminalMode.OriginMode, enabled);
                    return true;
                case 7: //Wraparound Mode (DECAWM)
                    setModeEnabled(TerminalMode.AutoWrap, enabled);
                    return true;
                case 8: //Auto-repeat Keys (DECARM)
                    setModeEnabled(TerminalMode.AutoRepeatKeys, enabled);
                    return true;
                case 12: //Start Blinking Cursor (att610)
                    //setModeEnabled(TerminalMode.CursorBlinking, enabled);
                    //We want to show blinking cursor always
                    return true;
                case 25:
                    setModeEnabled(TerminalMode.CursorVisible, enabled);
                    return true;
                case 40: //Allow 80->132 Mode
                    setModeEnabled(TerminalMode.AllowWideColumn, enabled);
                    return true;
                case 45: //Reverse-wraparound Mode
                    setModeEnabled(TerminalMode.ReverseWrapAround, enabled);
                    return true;
                case 47:
                case 1047:
                    setModeEnabled(TerminalMode.AlternateBuffer, enabled);
                    return true;
                case 1048:
                    setModeEnabled(TerminalMode.StoreCursor, enabled);
                    return true;
                case 1049: //Save cursor and use Alternate Screen Buffer
                    setModeEnabled(TerminalMode.StoreCursor, enabled);
                    setModeEnabled(TerminalMode.AlternateBuffer, enabled);
                    return true;
                case 1000:
                    if (enabled) {
                        setMouseMode(MouseMode.MOUSE_REPORTING_NORMAL);
                    } else {
                        setMouseMode(MouseMode.MOUSE_REPORTING_NONE);
                    }
                    return true;
                case 1001:
                    if (enabled) {
                        setMouseMode(MouseMode.MOUSE_REPORTING_HILITE);
                    } else {
                        setMouseMode(MouseMode.MOUSE_REPORTING_NONE);
                    }
                    return true;
                case 1002:
                    if (enabled) {
                        setMouseMode(MouseMode.MOUSE_REPORTING_BUTTON_MOTION);
                    } else {
                        setMouseMode(MouseMode.MOUSE_REPORTING_NONE);
                    }
                    return true;
                case 1003:
                    if (enabled) {
                        setMouseMode(MouseMode.MOUSE_REPORTING_ALL_MOTION);
                    } else {
                        setMouseMode(MouseMode.MOUSE_REPORTING_NONE);
                    }
                    return true;
                case 1005:
                    if (enabled) {
                        myTerminal.setMouseFormat(MouseFormat.MOUSE_FORMAT_XTERM_EXT);
                    } else {
                        myTerminal.setMouseFormat(MouseFormat.MOUSE_FORMAT_XTERM);
                    }
                    return true;
                case 1006:
                    if (enabled) {
                        myTerminal.setMouseFormat(MouseFormat.MOUSE_FORMAT_SGR);
                    } else {
                        myTerminal.setMouseFormat(MouseFormat.MOUSE_FORMAT_XTERM);
                    }
                    return true;
                case 1015:
                    if (enabled) {
                        myTerminal.setMouseFormat(MouseFormat.MOUSE_FORMAT_URXVT);
                    } else {
                        myTerminal.setMouseFormat(MouseFormat.MOUSE_FORMAT_XTERM);
                    }
                    return true;
                case 1034:
                    setModeEnabled(TerminalMode.EightBitInput, enabled);
                    return true;
                case 1039:
                    setModeEnabled(TerminalMode.AltSendsEscape, enabled);
                    return true;
                case 2004:
                    setModeEnabled(TerminalMode.BracketedPasteMode, enabled);
                    return true;
                default:
                    return false;
            }
        } else {
            switch (args.getArg(0, -1)) {
                case 2: //Keyboard Action Mode (AM)
                    setModeEnabled(TerminalMode.KeyboardAction, enabled);
                    return true;
                case 4: //Insert Mode (IRM)
                    setModeEnabled(TerminalMode.InsertMode, enabled);
                    return true;
                case 12: //Send/receive (SRM)
                    setModeEnabled(TerminalMode.SendReceive, enabled);
                    return true;
                case 20:
                    setModeEnabled(TerminalMode.AutoNewLine, enabled);
                    return true;
                case 25:
                    return true;
                default:
                    return false;
            }
        }
    }

    private boolean linePositionAbsolute(ControlSequence args) {
        int y = args.getArg(0, 1);
        myTerminal.linePositionAbsolute(y);

        return true;
    }

    private boolean restoreDecPrivateModeValues(ControlSequence args) {
        LOG.error("Unsupported: " + args.toString());

        return false;
    }

    private boolean deviceStatusReport(ControlSequence args) {
        if (args.startsWithQuestionMark()) {
            LOG.error("Don't support DEC-specific Device Report Status");
            return false;
        }
        int c = args.getArg(0, 0);
        if (c == 5) {
            String str = "\033[0n";
            LOG.debug("Sending Device Report Status : " + str);
            myTerminal.deviceStatusReport(str);
            return true;
        } else if (c == 6) {
            int row = myTerminal.getCursorY();
            int column = myTerminal.getCursorX();
            String str = "\033[" + row + ";" + column + "R";

            LOG.debug("Sending Device Report Status : " + str);
            myTerminal.deviceStatusReport(str);
            return true;
        } else {
            LOG.error("Sending Device Report Status : unsupported parameter: " + args.toString());
            return false;
        }
    }

    private boolean cursorShape(ControlSequence args) {
        myTerminal.cursorBackward(1);
        switch (args.getArg(0, 0)) {
            case 0:
            case 1:
                myTerminal.cursorShape(CursorShape.BLINK_BLOCK);
                return true;
            case 2:
                myTerminal.cursorShape(CursorShape.STEADY_BLOCK);
                return true;
            case 3:
                myTerminal.cursorShape(CursorShape.BLINK_UNDERLINE);
                return true;
            case 4:
                myTerminal.cursorShape(CursorShape.STEADY_UNDERLINE);
                return true;
            case 5:
                myTerminal.cursorShape(CursorShape.BLINK_VERTICAL_BAR);
                return true;
            case 6:
                myTerminal.cursorShape(CursorShape.STEADY_VERTICAL_BAR);
                return true;
            default:
                LOG.error("Setting cursor shape : unsupported parameter " + args.toString());
                return false;
        }
    }

    private boolean insertLines(ControlSequence args) {
        myTerminal.insertLines(args.getArg(0, 1));
        return true;
    }

    private boolean sendDeviceAttributes() {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Identifying to remote system as VT102");
        }
        myTerminal.deviceAttributes(CharUtils.VT102_RESPONSE);

        return true;
    }

    private boolean cursorHorizontalAbsolute(ControlSequence args) {
        int x = args.getArg(0, 1);

        myTerminal.cursorHorizontalAbsolute(x);

        return true;
    }

    private boolean cursorNextLine(ControlSequence args) {
        int dx = args.getArg(0, 1);
        dx = dx == 0 ? 1 : dx;
        myTerminal.cursorDown(dx);
        myTerminal.cursorHorizontalAbsolute(1);

        return true;
    }

    private boolean cursorPrecedingLine(ControlSequence args) {
        int dx = args.getArg(0, 1);
        dx = dx == 0 ? 1 : dx;
        myTerminal.cursorUp(dx);

        myTerminal.cursorHorizontalAbsolute(1);

        return true;
    }

    private boolean insertBlankCharacters(ControlSequence args) {
        final int count = args.getArg(0, 1);

        myTerminal.insertBlankCharacters(count);

        return true;
    }

    private boolean eraseInDisplay(ControlSequence args) {
        // ESC [ Ps J
        final int arg = args.getArg(0, 0);

        if (args.startsWithQuestionMark()) {
            //TODO: support ESC [ ? Ps J - Selective Erase (DECSED)
            return false;
        }

        myTerminal.eraseInDisplay(arg);

        return true;
    }

    private boolean eraseInLine(ControlSequence args) {
        // ESC [ Ps K
        final int arg = args.getArg(0, 0);

        if (args.startsWithQuestionMark()) {
            //TODO: support ESC [ ? Ps K - Selective Erase (DECSEL)
            return false;
        }

        myTerminal.eraseInLine(arg);

        return true;
    }

    private boolean deleteLines(ControlSequence args) {
        // ESC [ Ps M
        myTerminal.deleteLines(args.getArg(0, 1));
        return true;
    }

    private boolean deleteCharacters(ControlSequence args) {
        // ESC [ Ps P
        final int arg = args.getArg(0, 1);

        myTerminal.deleteCharacters(arg);

        return true;
    }

    private boolean cursorBackward(ControlSequence args) {
        int dx = args.getArg(0, 1);
        dx = dx == 0 ? 1 : dx;

        myTerminal.cursorBackward(dx);

        return true;
    }

    private boolean setScrollingRegion(ControlSequence args) {
        final int top = args.getArg(0, 1);
        final int bottom = args.getArg(1, myTerminal.getTerminalHeight());

        myTerminal.setScrollingRegion(top, bottom);

        return true;
    }

    private boolean scrollUp(ControlSequence args) {
        int count = args.getArg(0, 1);
        myTerminal.scrollUp(count);
        return true;
    }

    private boolean scrollDown(ControlSequence args) {
        int count = args.getArg(0, 1);
        myTerminal.scrollDown(count);
        return true;
    }

    private boolean cursorForward(ControlSequence args) {
        int countX = args.getArg(0, 1);
        countX = countX == 0 ? 1 : countX;

        myTerminal.cursorForward(countX);

        return true;
    }

    private boolean cursorDown(ControlSequence cs) {
        int countY = cs.getArg(0, 0);
        countY = countY == 0 ? 1 : countY;
        myTerminal.cursorDown(countY);
        return true;
    }

    private boolean cursorPosition(ControlSequence cs) {
        final int argy = cs.getArg(0, 1);
        final int argx = cs.getArg(1, 1);

        myTerminal.cursorPosition(argx, argy);

        return true;
    }

    private boolean characterAttributes(final ControlSequence args) {
        TextStyle styleState = createStyleState(myTerminal.getStyleState().getCurrent(), args);

        myTerminal.characterAttributes(styleState);

        return true;
    }

    @NotNull
    private static TextStyle createStyleState(@NotNull TextStyle textStyle, ControlSequence args) {
        TextStyle.Builder builder = textStyle.toBuilder();
        final int argCount = args.getCount();
        if (argCount == 0) {
            builder = new TextStyle.Builder();
        }

        int i = 0;
        while (i < argCount) {
            int step = 1;

            final int arg = args.getArg(i, -1);
            if (arg == -1) {
                LOG.error("Error in processing char attributes, arg " + i);
                i++;
                continue;
            }

            switch (arg) {
                case 0: //Normal (default)
                    builder = new TextStyle.Builder();
                    break;
                case 1:// Bold
                    builder.setOption(TextStyle.Option.BOLD, true);
                    break;
                case 2:// Dim
                    builder.setOption(TextStyle.Option.DIM, true);
                    break;
                case 3:// Italic
                    builder.setOption(TextStyle.Option.ITALIC, true);
                    break;
                case 4:// Underlined
                    builder.setOption(TextStyle.Option.UNDERLINED, true);
                    break;
                case 5:// Blink (appears as Bold)
                    builder.setOption(TextStyle.Option.BLINK, true);
                    break;
                case 7:// Inverse
                    builder.setOption(TextStyle.Option.INVERSE, true);
                    break;
                case 8: // Invisible (hidden)
                    builder.setOption(TextStyle.Option.HIDDEN, true);
                    break;
                case 22: //Normal (neither bold nor faint)
                    builder.setOption(TextStyle.Option.BOLD, false);
                    builder.setOption(TextStyle.Option.DIM, false);
                    break;
                case 23: // Not italic
                    builder.setOption(TextStyle.Option.ITALIC, false);
                    break;
                case 24: // Not underlined
                    builder.setOption(TextStyle.Option.UNDERLINED, false);
                    break;
                case 25: //Steady (not blinking)
                    builder.setOption(TextStyle.Option.BLINK, false);
                    break;
                case 27: //Positive (not inverse)
                    builder.setOption(TextStyle.Option.INVERSE, false);
                    break;
                case 28: //Visible, i.e. not hidden
                    builder.setOption(TextStyle.Option.HIDDEN, false);
                    break;
                case 30:
                case 31:
                case 32:
                case 33:
                case 34:
                case 35:
                case 36:
                case 37:
                    builder.setForeground(TerminalColor.index(arg - 30));
                    break;
                case 38: // Set xterm-256 text color
                    TerminalColor color256 = getColor256(args, i);
                    if (color256 != null) {
                        builder.setForeground(color256);
                        step = getColor256Step(args, i);
                    }
                    break;
                case 39: // Default (original) foreground
                    builder.setForeground(null);
                    break;
                case 40:
                case 41:
                case 42:
                case 43:
                case 44:
                case 45:
                case 46:
                case 47:
                    builder.setBackground(TerminalColor.index(arg - 40));
                    break;
                case 48: // Set xterm-256 background color
                    TerminalColor bgColor256 = getColor256(args, i);
                    if (bgColor256 != null) {
                        builder.setBackground(bgColor256);
                        step = getColor256Step(args, i);
                    }
                    break;
                case 49: //Default (original) foreground
                    builder.setBackground(null);
                    break;
                case 90:
                case 91:
                case 92:
                case 93:
                case 94:
                case 95:
                case 96:
                case 97:
                    //Bright versions of the ISO colors for foreground
                    builder.setForeground(ColorPalette.getIndexedTerminalColor(arg - 82));
                    break;
                case 100:
                case 101:
                case 102:
                case 103:
                case 104:
                case 105:
                case 106:
                case 107:
                    //Bright versions of the ISO colors for background
                    builder.setBackground(ColorPalette.getIndexedTerminalColor(arg - 92));
                    break;
                default:
                    LOG.error("Unknown character attribute:" + arg);
            }
            i = i + step;
        }
        return builder.build();
    }

    private static TerminalColor getColor256(ControlSequence args, int index) {
        int code = args.getArg(index + 1, 0);

        if (code == 2) {
            /* direct color in rgb space */
            int val0 = args.getArg(index + 2, -1);
            int val1 = args.getArg(index + 3, -1);
            int val2 = args.getArg(index + 4, -1);
            if ((val0 >= 0 && val0 < 256) &&
                    (val1 >= 0 && val1 < 256) &&
                    (val2 >= 0 && val2 < 256)) {
                return new TerminalColor(val0, val1, val2);
            } else {
                LOG.error("Bogus color setting " + args.toString());
                return null;
            }
        } else if (code == 5) {
            /* indexed color */
            return ColorPalette.getIndexedTerminalColor(args.getArg(index + 2, 0));
        } else {
            LOG.error("Unsupported code for color attribute " + args.toString());
            return null;
        }
    }

    private static int getColor256Step(ControlSequence args, int i) {
        int code = args.getArg(i + 1, 0);
        if (code == 2) {
            return 5;
        } else if (code == 5) {
            return 3;
        }
        return 1;
    }

    private boolean cursorUp(ControlSequence cs) {
        int arg = cs.getArg(0, 0);
        arg = arg == 0 ? 1 : arg;
        myTerminal.cursorUp(arg);
        return true;
    }

    private void setModeEnabled(final TerminalMode mode, final boolean enabled) {
        if (LOG.isDebugEnabled()) {
            LOG.info("Setting mode " + mode + " enabled = " + enabled);
        }
        myTerminal.setModeEnabled(mode, enabled);
    }

    public void setMouseMode(MouseMode mouseMode) {
        myTerminal.setMouseMode(mouseMode);
    }

    public @NotNull CompletableFuture<?> getPromptUpdatedAfterResizeFuture(@NotNull BiConsumer<Long, Runnable> taskScheduler) {
        CompletableFuture<Void> resizeFuture = new CompletableFuture<>();
        taskScheduler.accept(100L, this::completeResize);
        myResizeFutureQueue.add(resizeFuture);
        return resizeFuture;
    }

    private void completeResize() {
        CompletableFuture<Void> resizeFuture;
        while ((resizeFuture = myResizeFutureQueue.poll()) != null) {
            resizeFuture.complete(null);
        }
    }
}
